library hc_gesture_detector;

import 'dart:async';
import 'dart:math' as math;

import 'package:flutter/widgets.dart';
import 'package:vector_math/vector_math.dart' as vector;

class HcGestureDetector extends StatefulWidget {
  HcGestureDetector({
    @required this.child,
    this.onTap,
    this.onMoveUpdate,
    this.onMoveEnd,
    this.onMoveStart,
    this.onScaleStart,
    this.onScaleUpdate,
    this.onScaleEnd,
    this.onDoubleTap,
    this.bypassMoveEventAfterLongPress = true,
    this.bypassTapEventOnDoubleTap = false,
    this.doubleTapTimeConsider = 250,
    this.longPressTimeConsider = 350,
    this.onLongPress,
    this.onLongPressEnd,
  });

  final Widget child;

  /// 当双击事件发生时启用/禁用点击事件的标志。
  ///
  /// 默认为false，即当用户在屏幕上双击时，会触发1次双击事件和2次单点击事件
  final bool bypassTapEventOnDoubleTap;

  /// 默认为true，表示在收到长按事件而没有释放指针(手指仍触碰屏幕)后，移动事件将被忽略。
  ///
  /// 将其设置为false，以防在长按事件后触发移动事件
  ///
  final bool bypassMoveEventAfterLongPress;

  /// 检测双击的特定时间
  final int doubleTapTimeConsider;

  /// 之前触发onTapDown的指针也触发了onTapUp，最终触发onTap
  final TapEventListener onTap;

  /// A pointer has contacted the screen with a primary button and has begun to
  /// move.
  final MoveEventListener onMoveStart;

  /// 一个指针已经接触到屏幕上的一个主按钮，并开始移动
  final MoveEventListener onMoveUpdate;

  ///一个先前通过主元素与屏幕接触的指针
  ///按钮和移动不再与屏幕接触并被移动
  ///当它停止接触屏幕时以特定的速度。
  final MoveEventListener onMoveEnd;

  ///与屏幕接触的指针已经建立了一个焦点
  ///初始值 1.0。
  final void Function(Offset initialFocusPoint) onScaleStart;

  /// 缩放监听
  final ScaleEventListener onScaleUpdate;

  /// 指针不再与屏幕接触,缩放结束
  final void Function() onScaleEnd;

  /// 用户在同一位置连续两次点击屏幕。
  final TapEventListener onDoubleTap;

  /// 一个指针长时间保持与屏幕在同一位置的接触
  final TapEventListener onLongPress;

  /// 在onLongPress事件后，指针不再与屏幕接触。
  final Function() onLongPressEnd;

  /// 检测长按的特定持续时间
  final int longPressTimeConsider;

  @override
  _XGestureDetectorState createState() => _XGestureDetectorState();
}

enum _GestureState {
  PointerDown,
  MoveStart,
  ScaleStart,
  Scalling,
  LongPress,
  Unknown
}

class _XGestureDetectorState extends State<HcGestureDetector> {
  List<_Touch> touches = [];
  double initialScaleDistance = 1.0;
  _GestureState state = _GestureState.Unknown;
  Timer doubleTapTimer;
  Timer longPressTimer;
  Offset lastTouchUpPos = Offset(0, 0);

  @override
  Widget build(BuildContext context) {
    return Listener(
      child: widget.child,
      onPointerDown: onPointerDown,
      onPointerUp: onPointerUp,
      onPointerMove: onPointerMove,
      onPointerCancel: onPointerUp,
    );
  }

  void onPointerDown(PointerDownEvent event) {
    touches.add(_Touch(event.pointer, event.localPosition));

    if (touchCount == 1) {
      state = _GestureState.PointerDown;
      startLongPressTimer(TapEvent.from(event));
    } else if (touchCount == 2) {
      state = _GestureState.ScaleStart;
    } else {
      state = _GestureState.Unknown;
    }
  }

  void initScaleAndRotate() {
    initialScaleDistance =
        (touches[0].currentOffset - touches[1].currentOffset).distance;
  }

  void onPointerMove(PointerMoveEvent event) {
    final touch = touches.firstWhere((touch) => touch.id == event.pointer);
    touch.currentOffset = event.localPosition;
    cleanupTimer();

    switch (state) {
      case _GestureState.LongPress:
        if (widget.bypassMoveEventAfterLongPress) {
          touch.startOffset = touch.currentOffset;
        } else {
          switch2MoveStartState(touch, event);
        }
        break;
      case _GestureState.PointerDown:
        switch2MoveStartState(touch, event);
        break;
      case _GestureState.MoveStart:
        widget.onMoveUpdate?.call(MoveEvent(
          event.localPosition,
          event.position,
          event.pointer,
          delta: event.delta,
          localDelta: event.localDelta,
        ));
        break;
      case _GestureState.ScaleStart:
        touch.startOffset = touch.currentOffset;
        state = _GestureState.Scalling;
        initScaleAndRotate();
        if (widget.onScaleStart != null) {
          final centerOffset =
              (touches[0].currentOffset + touches[1].currentOffset) / 2;
          widget.onScaleStart(centerOffset);
        }
        break;
      case _GestureState.Scalling:
        if (widget.onScaleUpdate != null) {
          var rotation = angleBetweenLines(touches[0], touches[1]);
          final newDistance =
              (touches[0].currentOffset - touches[1].currentOffset).distance;
          final centerOffset =
              (touches[0].currentOffset + touches[1].currentOffset) / 2;

          widget.onScaleUpdate(ScaleEvent(
              centerOffset, newDistance / initialScaleDistance, rotation));
        }
        break;
      default:
        touch.startOffset = touch.currentOffset;
        break;
    }
  }

  void switch2MoveStartState(_Touch touch, PointerMoveEvent event) {
    state = _GestureState.MoveStart;
    touch.startOffset = event.localPosition;
    widget.onMoveStart?.call(
        MoveEvent(event.localPosition, event.localPosition, event.pointer));
  }

  double angleBetweenLines(_Touch f, _Touch s) {
    double angle1 = math.atan2(f.startOffset.dy - s.startOffset.dy,
        f.startOffset.dx - s.startOffset.dx);
    double angle2 = math.atan2(f.currentOffset.dy - s.currentOffset.dy,
        f.currentOffset.dx - s.currentOffset.dx);

    double angle = vector.degrees(angle1 - angle2) % 360;
    if (angle < -180.0) angle += 360.0;
    if (angle > 180.0) angle -= 360.0;
    return vector.radians(angle);
  }

  void onPointerUp(PointerEvent event) {
    touches.removeWhere((touch) => touch.id == event.pointer);

    if (state == _GestureState.PointerDown) {
      if (!widget.bypassTapEventOnDoubleTap || widget.onDoubleTap == null) {
        callOnTap(TapEvent.from(event));
      }
      if (widget.onDoubleTap != null) {
        final tapEvent = TapEvent.from(event);
        if (doubleTapTimer == null) {
          startDoubleTapTimer(tapEvent);
        } else {
          cleanupTimer();
          if ((event.localPosition - lastTouchUpPos).distanceSquared < 200) {
            widget.onDoubleTap(tapEvent);
          } else {
            startDoubleTapTimer(tapEvent);
          }
        }
      }
    } else if (state == _GestureState.ScaleStart ||
        state == _GestureState.Scalling) {
      state = _GestureState.Unknown;
      widget.onScaleEnd?.call();
    } else if (state == _GestureState.MoveStart) {
      state = _GestureState.Unknown;
      widget.onMoveEnd
          ?.call(MoveEvent(event.localPosition, event.position, event.pointer));
    } else if (state == _GestureState.LongPress) {
      widget.onLongPressEnd?.call();
      state = _GestureState.Unknown;
    } else if (state == _GestureState.Unknown && touchCount == 2) {
      state = _GestureState.ScaleStart;
    } else {
      state = _GestureState.Unknown;
    }

    lastTouchUpPos = event.localPosition;
  }

  void startLongPressTimer(TapEvent event) {
    if (widget.onLongPress != null) {
      if (longPressTimer != null) {
        longPressTimer.cancel();
        longPressTimer = null;
      }
      longPressTimer =
          Timer(Duration(milliseconds: widget.longPressTimeConsider), () {
        if (touchCount == 1 && touches[0].id == event.pointer) {
          state = _GestureState.LongPress;
          widget.onLongPress(event);
          cleanupTimer();
        }
      });
    }
  }

  void startDoubleTapTimer(TapEvent event) {
    doubleTapTimer =
        Timer(Duration(milliseconds: widget.doubleTapTimeConsider), () {
      state = _GestureState.Unknown;
      cleanupTimer();
      if (widget.bypassTapEventOnDoubleTap) {
        callOnTap(event);
      }
    });
  }

  void cleanupTimer() {
    if (doubleTapTimer != null) {
      doubleTapTimer.cancel();
      doubleTapTimer = null;
    }
    if (longPressTimer != null) {
      longPressTimer.cancel();
      longPressTimer = null;
    }
  }

  void callOnTap(TapEvent event) {
    if (widget.onTap != null) {
      widget.onTap(event);
    }
  }

  get touchCount => touches.length;
}

class _Touch {
  int id;
  Offset startOffset;
  Offset currentOffset;

  _Touch(this.id, this.startOffset) {
    this.currentOffset = startOffset;
  }
}

///当指针在位时，指针相对于设备移动
///与设备联系。
///
/// * [HcGestureDetector.onMoveUpdate]，它允许调用者被通知这些
/// 事件。
/// * [HcGestureDetector.onMoveStart]，它允许调用者在事件第一次发生时得到通知
/// * [HcGestureDetector.onMoveEnd]，它允许调用者在最后一个move事件发生后得到通知。
@immutable
class MoveEvent extends TapEvent {
  ///将[delta]转换为事件接收者的本地坐标
  ///系统根据[transform]。
  final Offset localDelta;

  /// 指针自上次移动以来的逻辑像素距离
  /// [MoveEvent].
  final Offset delta;

  const MoveEvent(
    Offset localPos,
    Offset position,
    int pointer, {
    this.localDelta = const Offset(0, 0),
    this.delta = const Offset(0, 0),
  }) : super(localPos, position, pointer);
}

/// 点移动
@immutable
class TapEvent {
  ///指针的唯一标识符，不重用。每一个新的
  ///指针向下事件。
  final int pointer;

  final Offset localPos;

  final Offset position;

  const TapEvent(this.localPos, this.position, this.pointer);

  static from(PointerEvent event) {
    return TapEvent(event.localPosition, event.position, event.pointer);
  }
}

/// 2点事件
@immutable
class ScaleEvent {
  /// 2个指针之间的中点
  final Offset focalPoint;

  /// 2个指针在当前指针和前一个指针之间的delta距离
  final double scale;

  ///以弧度为单位的旋转角度-使用 rotate
  final double rotationAngle;

  const ScaleEvent(this.focalPoint, this.scale, this.rotationAngle);
}

typedef ScaleEventListener = void Function(ScaleEvent event);

typedef TapEventListener = void Function(TapEvent event);

typedef MoveEventListener = void Function(MoveEvent event);
